<?php
namespace DRY\Record;

/**
 * DonReY Framework 2012 :: Record module - "Multi" record type
 * Each field is stored individually in a row, serialized as text. Any number of fields can exist for a record.
 * There are however limits on the length of the content stored in a field. This type of record should be used as an "Attributes" holder for another type of record.
 * This record type only supports "Solo" mode, since it's not possible to join the record with some other data, considering that the record's fields are multiple rows in a database
 *
 * @author Wavetrex <wavetrex@gmail.com>
 * @link http://www.donrey.net/Documentation/Structure/Modules/Record/single.html
 * @license [GPL v3] http://www.gnu.org/licenses/gpl-3.0.html
 * @version 1.0.dev
 * @package DonReY
 * @subpackage Module.Record
 */
class Type_Multi extends base
{
	/**
	 * Current state of the record:
	 * 'init' - Record object was just initialized, it doesn't contain any data (ID not known)
	 * 'create' - New data was assigned to the record (import or assign), without ID being known (ID not known)
	 * 'idset' - Record received it's ID, but still no data (not read, not new) (ID known)
	 * 'loaded' - All the existing fields associated with this ID are loaded. Next "Read" operation will be silently ignored (ID known)
	 * 'update' - Some fields have been changed since preload (new field, new value, or deleted field) - only relevant for lazy write mode
	 * 'delete' - Record has been deleted (solo) or is being scheduled for deletion (set). This mode is final, all other operations will refuse once record is deleted
	 * @var string
	 */
	protected $_state = 'init';

	/**
	 * How to operate field read access:
	 *   "preload" Read and cache all fields when the record receives it's ID
	 *   "direct" Read a non-cached field only when it's accessed
	 * @var string
	 */
	protected $_fieldReadMode = 'preload';

	/**
	 * How to operate field write access:
	 *   "immediate", the field is written, updated or deleted immediatly
	 *   "lazy", the field is cached locally and written only at destruct time
	 * @var string
	 */
	protected $_fieldWriteMode = 'immediate';

	/**
	 * State of fields (so _GET / _SET know what to do ):
	 * 'new' - New field. Unknown if it already exists in database or not (an insert will be attempted)
	 * 'sync' - Last field read attempt succeeded, there is a field under this ID, also cached in $data, or it has just been forcibly written (ID known)
	 * 'readfail' - Attempted to read this field in the record, but it wasn't found (ID known)
	 * 'update' - Field's content changed since it was read - At destruct time this field will be deleted and recreated
	 * 'delete' - Field is scheduled for deletion at record destruct time
	 * @var array
	 */
	protected $_fieldState = array();


	/**
	 * "Multi" type record constructor
	 * @param \DRY\Module\Record $rec
	 * @param string $model
	 * @param mixed $id
	 * @param bool $set Mode of operation ( Solo or Set )
	 */
	public function __construct(\DRY\Module\Record $rec, $model, $id=null, $set = false)
	{
		$_def = &$rec-> $model;
		if(isset($_def['field_mode'])) {
			$this->_fieldReadMode = $_def['field_mode'][0];
			$this->_fieldWriteMode = $_def['field_mode'][1];
		};

		parent::__construct($rec, $model, $id, $set);

		// when some fields are defined, preinitialize those in the data
//		if(isset($this->_def['fields']))
//			foreach(array_keys($this->_def['fields']) as $field)
//				$this->_data[$field] = null;
	}

	/**
	 * @see DRY_Record_base::__get()
	 */
	public function __get($field)
	{
		if($field == 'id')
			return $this-> get_ID();

		switch($this->_state) {
			case 'init':
			case 'delete':
				// can't read fields when ID is not known, or it is known that all data has been removed
				return null;
			case 'loaded':
				// when all fields have been preloaded, don't attempt to individually read just one, we know exactly if it exists or not in database
				return isset($this->_fieldState[$field])? $this->_data[$field]: null;
		}

		// unknown if this field exists or not, it's not yet in $_data, so attempt to read it
		if(!isset($this->_fieldState[$field]))
			if($this->_fieldReadMode == 'direct')
				$this-> read_Field($field);

		switch($this->_fieldState[$field]) {
			// when last field read failed
			case 'readfail':
			case 'delete':
				return null;
		}

		// in other cases, return the field's value
		return $this->_data[$field];
	}

	/**
	 * Attempts to retrieve the ID of this record
	 * @return int|string Content of the ID field, or null if there is none
	 */
	public function get_ID()
	{
		switch($this->_state) {
			case 'init':
			case 'delete':
				// no ID
				return null;
			case 'create':
				// no ID yet, but there is some data, so attempt to get an ID by inserting an "ID" field in the database in order to find an InsertID
				$q = $this->_rec['queryfactory']-> insert()
					-> table($this->_def['table'])
					-> values(array(
						'field'=> $this->_def['id_field'],
						'value'=> '#ID#'
					));
				$this->_rec['connection']-> query($q);
				if($this->_rec['connection']-> insert_id == 0)
					throw new \DRY\Water('record', \DRY\Module\Record::EXC_INSERT_ID_NOT_RETURNED);
				$this->_id = $this->_rec['connection']-> insert_id;
				return $this->_id;
			default:
				// in other cases, there is an ID
				return $this->_id;
		}
	}

	/**
	 * @see DRY_Record_base::__set()
	 */
	public function __set($field, $value)
	{
		if($field == 'id') {
			$this-> set_ID($value);
			return;
		}

		// if there's no information about this field yet, attempt to use as "new field", or when field was previously deleted, assign operation recreates it
		if(!isset($this->_fieldState[$field]) || $this->_fieldState[$field]=='delete')
			$this->_fieldState[$field] = "new";

		switch($this->_fieldState[$field]) {
			case 'sync':
			case 'update':
				if($value==$this->_data[$field])
					return; // nothing to do here, value hasn't changed
				$this->_fieldState[$field] = 'update';
			case 'new':
			case 'readfail':
				// when a field is not in database, attempt to write it
				$this->_data[$field] = $value;
				if($this->_fieldWriteMode == 'immediate')
					$this-> write_Field($field);
				else {
					if($this->_fieldState[$field]=='readfail')
						$this->_fieldState[$field] = 'new';
					if($this->_fieldState[$field]=='sync')
						$this->_fieldState[$field] = 'update';
				}
				$this->_state = 'update';
				break;
		}
	}

	/**
	 * Sets the record's ID if it didn't previously exist
	 * @param int|string $id
	 * @param bool $force Forcibly changes or sets the ID. DON'T USE! Internal for RecordSet implementation
	 */
	public function set_ID($id, $force = false)
	{
		if(!$force)
		switch($this->_state) {
			case 'init':
				// if ID is set after init, change state to "idset" to indicate that the record is ready for field read/write operations
				$this->_state = 'idset';
				break;
			case 'delete':
				throw new \DRY\Water('record', \DRY\Module\Record::EXC_RECORD_ALREADY_DESTROYED);
			default:
				// in all other cases, the call is ignored because ID cannot be changed once it's been set for the same object
				return false;
		}
		$this->_id = $id;
		// preload the record if working in preload field read mode
		if($this->_fieldReadMode == 'preload')
			$this-> read();
	}

	/**
	 * Remove a field from the record by simply unsetting it.
	 * In immediate mode it's deleted from the database, in lazy mode it's just scheduled for deletion at destruction time.
	 * @param string $field
	 */
	public function __unset($field)
	{
		if($field == 'id')
			return; // can't delete the "id" field ( can only destroy the entire record with this id, use ::destroy() method

		$this->_fieldState[$field] = 'delete';
		if($this->_fieldWriteMode == 'immediate')
			$this-> write_Field($field);
		unset($this->_data[$field]);
		$this->_state = 'update';

		dump($this->_data);
	}

	/**
	 * This record type performs a write operation when being destructed, if working on Solo mode
	 */
	public function __destruct()
	{
		$this-> write(); // write if necessary, not forced
	}

	/**
	 * Reads all the fields that exist (preload), optionally overwriting current values
	 * @see DRY_Record_base::read()
	 * @throws Water
	 */
	public function read($overwrite=false)
	{
		switch($this->_state) {
			case 'idset':
			case 'update':
				// read all fields
				$q = $this->_rec['queryfactory']-> select()
					-> table($this->_def['table'], array('field','value'))
					-> where('id = ?', $this->_id);

				$raw_data = $this->_rec['connection']-> query($q);
				// it is possible that there are no fields (yet)
				foreach($raw_data as $field_entry) {
					$field = $field_entry['field'];
					if($field == 'id')
						continue;
					$this->_fieldState[$field] = 'sync';
					if(!isset($this->_data[$field]) || $overwrite) // only overwrite if requested (or when field doesn't already exist)
						$this->_data[$field] = $this-> convert_Raw($field, $field_entry['value']);
				}
				$this->_state = 'loaded';
				break;
			case 'init':
			case 'create':
			case 'loaded':
			case 'delete':
				// Nothing to do here, either ID doesn't exist yet, or the entire record is in sync. Also, read is not allowed when record is set for deletion
				break;
			default:
				// fatal error if the record is not in one of the existing "states"
				throw new \DRY\Water('record', \DRY\Module\Record::EXC_INVALID_RECORD_STATE);
		}
	}

	/**
	 * Attempts to read a single field from the table. This method is only for fieldReadMode = 'direct'
	 * @param string $field Name
	 * @param bool $force Attempt to read again, even if last read operation failed
	 */
	protected function read_Field($field, $force = false)
	{
		// record mode
		switch($this->_state) {
			case 'init':
			case 'create':
				// nothing to do here, since ID is not known
				return null;
			case 'loaded':
				// attempt to read again only if forced
				if(!$force)
					return $this->_data[$field];
			case 'idset':
			case 'update':
				// proceed with reading (if necessary)
				break;
			case 'delete':
				return false; // can't read a field if the record has already been deleted
			default:
				// fatal error if the record is not in one of the existing "states"
				throw new \DRY\Water('record', \DRY\Module\Record::EXC_INVALID_RECORD_STATE);
		}

		// field mode
		if(isset($this->_fieldState[$field]))
		switch($this->_fieldState[$field]) {
			case 'readfail':
				// when last read failed, return null if not forced to read again
				if(!$force)
					return null;
			case 'sync':
			case 'update':
			case 'new':
				// when synced, don't attempt to read unless forced - For "new" and "update", force will actually overwrite existing value in $_data
				if(!$force)
					return $this->_data[$field];
			case 'delete':
				return null; // this field no longer exists
		}

		$q = $this->_rec['queryfactory']-> select()
			-> table($this->_def['table'], array('value'))
			-> limit(1)
			-> where('id = ?', $this->_id)
			-> where('field = ?', $field);

		$raw_data = $this->_rec['connection']-> query($q)-> current();

		if(empty($raw_data)) {
			$this->_fieldState[$field] = 'readfail';
			return null;
		}
		$this->_fieldState[$field] = 'sync';
		$this->_data[$field] = $this-> convert_Raw($field, $raw_data['value']);
	}

	/**
	 * Alternate method of populating the record with data, by searching for arbitrary conditions.
	 * The result of this method is the discovery of a valid record ID, so subsequent read/write operations can succeed
	 * @param callback $callback Callback to a method that receives the QueryFactory object, to put arbitrary conditions.
	 */
	public function search($callback)
	{
		// this method is for Solo mode.
		if($this->_setMode == true) return;

		if($this->_state == 'delete')
			throw new \DRY\Water('record', \DRY\Module\Record::EXC_RECORD_ALREADY_DESTROYED);

		if(is_callable($callback)) {
			$q = $this->_rec['queryfactory']-> select()
				-> table($this->_def['table'])
				-> limit(1);
			// use the callback to add aditional filters (where, order, perhaps joins?) to this read
			$callback($q);
			$raw_found_field = $this->_rec['connection']-> query($q)-> current();
			if(!empty($raw_found_field)) {
				// when found, reset existing record data
				$this->_state = 'init';
				$this->_fieldState = array();
				$this->_data = array();
				$this-> set_ID($raw_found_field['id']);
				return true;
			} else
				return false; // not found, the record is unaffected
		} else
			return false; // invalid callback, can't search without one
	}

	/**
	 * TODO: Write for lazy mode
	 * Intelligent write operation (create, update, delete fields), depending on the record's state and field states ( for "lazy" write mode ).
	 * In direct mode, nothing should happen, since all fields are synced
	 * Note:After a write operation, the record MUST have an ID, if there was a fatal error in an Insert operation and the ID cannot be created, throw an exception
	 * @see DRY_Record_base::write()
	 */
	public function write($force=false)
	{
	}

	/**
	 * Attempts to write a single field in the table. This method is only for fieldWriteMode = 'immediate'
	 * @param string $field Name
	 * @param bool $force Force writing this field, even if it's known that it exists in the database and has current value (just read or written)
	 */
	protected function write_Field($field, $force = false)
	{
		// record mode
		switch($this->_state) {
			case 'init':
				$this->_state = 'create';
			case 'create':
				// attempt to get an ID for this record if none exists yet
				$this-> get_ID();
			case 'update':
			case 'idset':
			case 'loaded':
				// nothing special to do here
				break;
			case 'delete':
				return false; // can't write if record is in "delete" mode
			default:
				// fatal error if the record is not in one of the existing "states"
				throw new \DRY\Water('record', \DRY\Module\Record::EXC_INVALID_RECORD_STATE);
		}

		if(!isset($this->_fieldState[$field]))
			return false; // can't write a field which does not exist

		// field mode
		switch($this->_fieldState[$field]) {
			case 'readfail':
				$write_mode = 'insert';
				break;
			case 'sync':
				// when synced, doesn't attempt to write unless forced
				if(!$force)
					return false;
			case 'new':
			case 'update':
				$write_mode = 'update';
				break;
			case 'delete':
				$write_mode = 'delete';
				break;
		}

		// try to update the field first
		if($write_mode == 'update') {
			$q = $this->_rec['queryfactory']-> update()
				-> table($this->_def['table'])
				-> values(array(
					'value'=> $this-> convert_Internal($field)
				))
				-> where('id = ?', $this->_id)
				-> where('field = ?', $field);
			$this->_rec['connection']-> query($q);
			// see what happened
			$sql_response = $this->_rec['connection']-> get_Response();
			if($sql_response['rows_matched']==0)
				$write_mode = 'insert'; // oops, update failed, possibly because the row doesn't yet exist, so try to insert
			$this->_fieldState[$field] = 'sync';
		}

		// if update didn't work or insert was specifically asked, do so
		if($write_mode == 'insert') {
			$q = $this->_rec['queryfactory']-> insert()
				-> table($this->_def['table'])
				-> values(array(
					'id'=> $this->_id,
					'field'=> $field,
					'value'=> $this-> convert_Internal($field)
				));
			$this->_rec['connection']-> query($q);
			$this->_fieldState[$field] = 'sync';
		}

		// field is going to be deleted
		if($write_mode == 'delete') {
			$q = $this->_rec['queryfactory']-> delete()
				-> table($this->_def['table'])
				-> where('id = ?', $this->_id)
				-> where('field = ?', $field);
			$this->_rec['connection']-> query($q);
			unset($this->_fieldState[$field]); // field no longer exist
		}

		return true;
	}

	/**
	 * @see DRY_Record_base::destroy()
	 */
	public function destroy()
	{
	}

//===== Database operations, Set mode =====//

	/**
	 * No, "Multi" record can only read/write records individually, so basically even in "Set" mode they behave the same like in "Solo" mode
	 * @see DRY_Record_base::_implements_Join()
	 */
	public static function _implements_Join()
	{
		return false;
	}

	/**
	 * Since no joined-mode support, this function actually does the writing when called (just like __destruct() for Solo mode)
	 * @see DRY_Record_base::_prepare_WriteContent()
	 */
	public function _prepare_WriteContent()
	{
		$this-> write();
	}

}